package org.ramoths.netcom.sessions;

import org.ramoths.netcom.sessions.exceptions.IOExceptionDistante;
import org.ramoths.netcom.sessions.exceptions.IOExceptionFermeture;
import org.ramoths.netcom.sessions.verrous.VrDonneesDistantes;
import org.ramoths.netcom.sessions.verrous.VrDonneesOutputStream;

import java.io.IOException;
import java.io.OutputStream;

public class OutputStreamConsommateur extends OutputStream
{
	private final SessionCommunication sessionComm;
	private final int idOutputStream;
	//
	private final byte[] donnees;
	private int indexe;
	private int taille;
	private boolean fin;
	private IOException exception;
	private final VrDonneesOutputStream vrDonneesOutputStream;
	//
	private Exception exceptionArrivee;
	private final VrDonneesDistantes vrDonneesDistantes;
	//
	private final float tauxMinimum;
	//
	private final ThOutputStreamConsommateur thOutputStreamConsommateur;
	
	public OutputStreamConsommateur(SessionCommunication sessionComm, int idOutputStream, int tailleTempon, float tauxMinimum)
	{
		this.sessionComm = sessionComm;
		this.idOutputStream = idOutputStream;
		//
		this.donnees = new byte[tailleTempon];
		this.indexe = 0;
		this.taille = 0;
		this.fin = false;
		this.exception = null;
		this.vrDonneesOutputStream = new VrDonneesOutputStream();
		//
		this.exceptionArrivee = null;
		this.vrDonneesDistantes = new VrDonneesDistantes();
		//
		this.tauxMinimum = tauxMinimum;
		//
		this.thOutputStreamConsommateur = new ThOutputStreamConsommateur(this);
		this.thOutputStreamConsommateur.start();
	}
	
	@Override
	public void write(int b) throws IOException
	{
		vrDonneesOutputStream.lock();
		try
		{
			while (!fin && exception == null && taille == donnees.length)
			{
				vrDonneesOutputStream.getConditionTauxMinimumAtteint().signal();
				vrDonneesOutputStream.getConditionPlaceDisponible().awaitUninterruptibly();
			}
			if (!fin && exception == null && (float) (donnees.length-taille) / donnees.length <= tauxMinimum)
				vrDonneesOutputStream.getConditionTauxMinimumAtteint().signal();
			if (taille < donnees.length)
			{
				donnees[indexe] = (byte) b;
				indexe = (indexe + 1) % donnees.length;
				taille += 1;
			}
			else if (exception != null)
			{
				throw exception;
			}
			else // Fin
			{
				throw new IOExceptionFermeture("Flux fermé!");
			}
		}
		finally
		{
			vrDonneesOutputStream.unlock();
		}
	}
	
	void runThOutputStreamConsommateur()
	{
		byte[] tmp = null;
		//
		while (true)
		{
			/*
			 * Attente de déclenchement
			 */
			vrDonneesOutputStream.lock();
			try
			{
				try
				{
					if ((float) (donnees.length-taille) / donnees.length > tauxMinimum)
						vrDonneesOutputStream.getConditionTauxMinimumAtteint().await();
				}
				catch (InterruptedException ex)
				{
					break;
				}
				//
				if (taille == 0)
					continue;
				//
				tmp = new byte[taille];
				if (indexe + taille <= donnees.length)
				{
					System.arraycopy(donnees, indexe, tmp, 0, taille);
				}
				else
				{
					System.arraycopy(donnees, indexe, tmp, 0, donnees.length-indexe);
					System.arraycopy(donnees, 0, tmp, donnees.length-indexe, taille-donnees.length+indexe);
				}
				taille=0;
				vrDonneesOutputStream.getConditionPlaceDisponible().signalAll();
			}
			finally
			{
				vrDonneesOutputStream.unlock();
			}
			/*
			 * Envoi des donnees
			 */
			vrDonneesDistantes.lock();
			try
			{
				sessionComm.oscEnvoyerDonneesOutputStream(idOutputStream, tmp);
				vrDonneesDistantes.getConditionDonneesVenues().awaitUninterruptibly();
			}
			finally
			{
				vrDonneesDistantes.unlock();
			}
			/*
			 * Intégration de la réponse
			 */
			vrDonneesOutputStream.lock();
			try
			{
				if (exception == null && exceptionArrivee != null)
					exception = new IOExceptionDistante("Erreur distante d'écriture dans le flux!", exceptionArrivee);
				//
				vrDonneesOutputStream.getConditionPlaceDisponible().signalAll();
				//
				if (fin || exception != null)
					break;
			}
			finally
			{
				vrDonneesOutputStream.unlock();
			}
		}
	}
	
	void setDonnees(Exception exception)
	{
		vrDonneesDistantes.lock();
		try
		{
			exceptionArrivee = exception;
			vrDonneesDistantes.getConditionDonneesVenues().signal();
		}
		finally
		{
			vrDonneesDistantes.unlock();
		}
	}
	
	@Override
	public void flush() throws IOException
	{
		vrDonneesOutputStream.lock();
		try
		{
			if (fin || exception != null)
				throw new IOExceptionFermeture("Flux fermé!");
			//
			vrDonneesOutputStream.getConditionTauxMinimumAtteint().signal();
			vrDonneesOutputStream.getConditionPlaceDisponible().awaitUninterruptibly();
			//
			if (exception != null)
				throw exception;
			if (fin)
				throw new IOExceptionFermeture("Flux fermé!");
		}
		finally
		{
			vrDonneesOutputStream.unlock();
		}
	}
	
	@Override
	public void close() throws IOException
	{
		thOutputStreamConsommateur.interrupt();
		boolean interruption = false;
		while (true)
		{
			try
			{
				thOutputStreamConsommateur.join();
				break;
			}
			catch (InterruptedException ex)
			{
				interruption = true;
			}
		}
		if (interruption)
			Thread.currentThread().interrupt();
		/*
		 * Vérification
		 */
		vrDonneesOutputStream.lock();
		try
		{
			if (fin || exception != null)
				return;
		}
		finally
		{
			vrDonneesOutputStream.unlock();
		}
		/*
		 * Envoi de l'ordre de fermeture
		 */
		vrDonneesDistantes.lock();
		try
		{
			sessionComm.oscEnvoyerFermeture(idOutputStream);
			vrDonneesDistantes.getConditionDonneesVenues().awaitUninterruptibly();
		}
		finally
		{
			vrDonneesDistantes.unlock();
		}
		/*
		 * Intégration des données
		 */
		vrDonneesOutputStream.lock();
		try
		{
			if (exceptionArrivee == null)
			{
				fin = true;
			}
			else
			{
				exception = new IOExceptionDistante("Erreur distante de fermeture du flux!", exceptionArrivee);
			}
			//
			vrDonneesOutputStream.getConditionPlaceDisponible().signalAll();
		}
		finally
		{
			vrDonneesOutputStream.unlock();
		}
	}
	
	public int getIdOutputStream()
	{
		return idOutputStream;
	}
}
